NumPy + TD1 *********** Vous pouvez tester en ligne les exemples grâce aux sites suivant : * https://www.online-Python.com/ * https://replit.com/languages/Python3 * https://www.onlinegdb.com/online_Python_compiler Vous pouvez retrouver les thèmes abordés dans cette page dans la documentation de NumPy au chapitre `NumPy fundamentals `_ . La libraire NumPy offre des facilités d'écriture permettant d'effectuer des traitements complexes sur des tableaux en une seule ligne de code. Cette syntaxe compacte permet d'atteindre des temps d'exécution très performants en Python ce qui n'aurait pas été possible si l'on avait du écrire de nombreuses boucles imbriquées. Les tableaux NumPy ================== Pour stocker des données (images, sons, textes...), nous utilisons des tableaux multidimensionnels. Par exemple, un lot de 50 000 images de résolution 36x36 sera stocké dans un tableau de dimension 50 000x36x36. Les listes Python, même si elles peuvent faire office de tableaux multidimensionnels, ne sont pas des objets optimisés en mémoire et en vitesse, elles doivent donc être évitées. Pour stocker des données, le choix se porte vers les tableaux de la librairie NumPy. Les tableaux NumPy offrent des performances équivalentes aux tableaux du langage C, mais à travers l’interpréteur Python. Contrairement aux listes Python qui sont des containers dynamiques stockant des objets de types hétérogènes, **les tableaux Numpy ont une taille fixe et stockent des valeurs de même type**. La performance et la célébrité grandissante de NumPy ont amené les tableaux NumPy à devenir les briques de base d’autres librairies comme TensorFlow ou OpenCV. Étant quasiment devenu un standard, les tenseurs des librairies Keras, TensorFlow ou Pytorch ont repris les principes de fonctionnement des tableaux NumPy. .. note:: `La documentation des tableaux NumPy `_ est disponible en ligne, elle est claire et bien organisée. Création ======== De nombreuses fonctions permettent de créer et d'initialiser un tableau: * `Les fonctions de création. `_ * `Les fonctions de génération aléatoire. `_ .. code-block:: python import numpy as np np.zeros(shape=(3,2)) # crée un tableau de taille (3,2) rempli de float64 valant 0 >> array([[0., 0.],[0., 0.],[0., 0.]]) np.zeros(shape=(3,2),dtype=np.float32) # crée un tableau de taille (3,2) rempli de float32 valant 0 >> array([[0., 0.],[0., 0.],[0., 0.]]) np.zeros(shape=(3,2),dtype=np.int32) # crée un tableau de taille (3,2) rempli de int32 valant 0 >> array([[0, 0],[0, 0],[0, 0]]) np.ones(shape=(3,2)) # crée un tableau de taille (3,2) rempli de 1 >> array([[1., 1.], [1., 1.], [1., 1.]]) np.empty(shape=(3,2)) # crée un tableau sans initialiser les valeurs np.random.randint(low=3, high=8, size=(2,4)) # tableau avec valeurs aléatoires parmi 3, 5, 5, 6 et 7. >> array([[5, 5, 3, 6], [7, 4, 6, 3]]) np.random.rand(3,2) # tableau avec valeurs aléatoires dans l'intervalle [0,1[ avec distribution uniforme, >> array([[0.5337, 0.42173, 0.66, 0.012], [0.2962, 0.88, 0.08020, 0.061]]) np.random.normal(loc=0,scale=1,size=(3,2)) # tableau avec valeurs aléatoires suivant une loi normale(0,1) >> array([[-0.9797, 1.54788 ], [ 0.1402, -0.217], [ 0.3213571, -0.751978]]) np.arange(10) # crée un tableau contenant les nombres de 0 à 10 (non compris) >> array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) .. warning:: La taille d'un tableau lors de sa création est donnée par des arguments ayant des noms différents: *size* pour *np.random.randint()*, *shape* pour *np.ones()* ou *d0, d1, ...* *pour np.random.rand()*. On peut créer un nouveau tableau par recopie d'un tableau existant avec la fonction **copy()**. Les données du nouveau tableau sont alors indépendantes du tableau précédent : Il est préférable de toujours fournir le type des données car par défaut Numpy utilise des float64. Pour connaître le format des données utilisé dans un tableau on peut utiliser le paramètres dtype: : .. code-block:: python import numpy as np T = np.zeros(shape=(3,2)) print(T.dtype) >> float64 .. code-block:: python import numpy as np A = np.zeros(10) B = A.copy() # création du tableau B par recopie du tableau A B[1] = 7 A >> array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) B >> array([0., 7., 0., 0., 0., 0., 0., 0., 0., 0.]) Conversion ========== Les fonctions de conversion et de redimensionnement sont listées dans la documention au chapitre `Array manipulation routines `_. Rappel sur les listes --------------------- Bien que nous n'utilisons pas les listes pour stocker les données d'apprentissage, on en trouve souvent dans les tutoriaux car elles permettent d'écrire facilement des données. En Python, les listes sont définies avec des paires de crochets englobant une série de nombres. .. code-block:: python L = [ 1, 2, 3, 5, 8] # liste L = [ [1,2,3,4], [5,6,7,8]] # liste de listes Conversion liste/tableau ------------------------ Il est possible de convertir une liste en tableau et réciproquement : .. code-block:: python A = np.array(L) # convertit une liste Python en NumPy Array L = A.tolist() # convertit un NumPy array en liste Python Conversion de type ------------------ Il est possible de copier et de caster les valeurs d'un tableau grâce à la fonction *astype* : .. code-block:: python A = np.array([0,0,0,0]) A.astype(np.float32) >> array([0., 0., 0., 0.], dtype=float32) Changement de dimension ----------------------- Il est possible de modifier les dimensions d'un tableau si le nouveau tableau comporte exactement le même nombre d'éléments que l'ancien. .. code-block:: python A = np.arange(6) >> array([0, 1, 2, 3, 4, 5]) B = A.reshape(2,3) # A n'est pas modifié, B >> array([[0, 1, 2], [3, 4, 5]]) C = B.reshape((3,2)) C >> array([[0, 1], [2, 3], [4, 5]]) .. note:: Si vous lisez les éléments des trois tableaux de gauche à droite et de haut en bas, vous lisez les mêmes valeurs dans le même ordre : 0, 1, 2, 3, 4 et 5. .. quiz:: aaazeaa :title: Création des tableaux NumPy :quiz:`{"type":"FB","answer":"D"}` Quelle fonction crée un tableau NumPy initialisé avec des 1 ? A: init() - B: one() - C: set() - D: ones() :quiz:`{"type":"FB","answer":"B"}` Quelle fonction crée un tableau NumPy initialisé avec des 0 ? A: init() - B: zeros() - C: set() - D: zero() :quiz:`{"type":"FB","answer":"B"}` Quel nom d'argument permet de sélectionner le type des données ? A: type - B: dtype - C: id - D: np :quiz:`{"type":"FB","answer":"random"}` Quel est le nom du package contenant la fonction rand() ? :quiz:`{"type":"FB","answer":"C"}` Quelle fonction crée un tableau NumPy depuis une liste Python ? A: fromList() - B: arange() - C: array() - D: initFrom() :quiz:`{"type":"FB","answer":"A"}` Quel est le nom de la fonction de conversion de type ? A: astype() - B: reshape() - C: array() - D: from() :quiz:`{"type":"FB","answer":"B"}` Quelle fonction permet de changer les dimensions d'un tableau ? A: setshape() - B: reshape() - C: range() - D: redim() :quiz:`{"type":"TF","answer":"F"}` Peut-on effectuer un reshape((2,3)) sur un tableau A = np.arange(5) ? Les vues ======== Dans un problème de classification d'images, la base d'images sera chargée dans un tableau Numpy. Ainsi, les tableaux NumPy permettent de stocker plusieurs Gigas de données. Lorsque l'on extrait des données d'un tableau, par exemple en prenant 20% de la base pour crée un set de validation, on prend le risque de dédoubler les données. Afin d'éviter cette situation, un mécanisme spécial a été mis en place afin d'optimiser les ressources mémoire. Ainsi, un tableau NumPy a deux états possibles : soit il dispose de son propre buffer de données, soit il fait un lien vers le buffer d'un autre tableau et dans ce cas précis, le tableau Numpy s'appelle une **vue**. La non-connaissance de ce mécanisme peut entraîner de nombreux problèmes pour le développeur, surtout dans nos exercices. En effet, comme la vue et le tableau associé à la vue partagent le même buffer de données, toute modification depuis la vue modifie le tableau d'origine et inversement. Pour tester si un tableau NumPy correspond à une vue, on utilise l'attribut *base* du tableau qui indique si un lien existe vers au autre tableau. Ainsi le test **x.base is not None** permet de savoir si le tableau est une copie. .. code-block:: python x = np.arange(4) >> array([0, 1, 2, 3]) x.base is not None >> False # ce tableau dispose de son propre buffer y = x.view() # y est une vue du tableau x y >> array([0, 1, 2, 3]) y.base is not None >> True # y est est une vue y[0] = 8 y >> array([8, 1, 2, 3]) x >> array([8, 1, 2, 3]) # le tableau x sous-jacent a été aussi modifié x[3] = 9 array([8, 1, 2, 9]) y # et la vue y a été impactée >> array([8, 1, 2, 9]) Prenons maintenant le cas de la fonction *reshape()*. Cette fonction permet de construire à partir d'un tableau existant un nouveau tableau de dimensions différentes. Dans ce cas précis, les données ne changent pas, ainsi la fonction *reshape()* a tout intérêt à retourner une vue et, en pratique, dans les cas simples, la fonction *reshape()* va effectivement fournir une vue. .. code-block:: python x = np.arange(4) >> array([0, 1, 2, 3]) y = x.reshape((2,2)) >> array([[0, 1], [2, 3]]) y.base is not None >> True # y est une vue y[0,0] = 9 y >> array([[9, 1], [2, 3]]) x # le tableau référencé a été modifié dans la foulée >> array([9, 1, 2, 3]) Mais, dans des situations plus complexes (des vues sur des sélections de vues), il est possible que la fonction *reshape()* n'arrive pas à construire une vue et qu'elle doive se résoudre à construire un tableau ayant son propre buffer de données. La documentation de NumPy exprime clairement cette situation : **The NumPy.reshape() function creates a view where possible or a copy otherwise**. Pour la première fois en informatique, on se retrouve dans une situation assez inhabituelle : une fonction retourne un nouvel objet qui peut être une copie (donc indépendant) ou une vue (similaire à une référence). Ce choix est guidé par le bon sens, mais la décision de ce mécanisme semble complètement opaque. En ce qui concerne l'apprentissage, ne nous rencontrerons généralement pas de problème, car en effet, les tableaux sont souvent accédés en lecture, cette ambiguïté reste alors sans conséquence. Dans ce chapitre, les fonctions retournant toujours une copie ou toujours une vue seront précisées à titre indicatif. .. note:: Pour les tenseurs des librairies d'IA comme Pytorch ou TensorFlow, la logique des vues est reprise à l'identique car la taille mémoire d'un GPU est encore plus contrainte que celle d'un ordinateur. Les besoins d’optimisation mémoire sont donc tout autant présent. Propriétés ========== Taille ------ La propriété **shape** nous permet de connaître la taille d'un tableau NumPy: .. code-block:: python import numpy as np A = np.array([ [[0,0,0,0],[1,1,1,1],[2,2,2,2]], [[0,0,0,0],[1,1,1,1],[2,2,2,2]] ]) print(A.shape) ==> (2, 3, 4) Si nous examinons la liste de listes de listes Python : [ [ [0,0,0,0], [1,1,1,1], [2,2,2,2] ], [ [0,0,0,0], [1,1,1,1], [2,2,2,2] ] ], le plus bas niveau est constitué de listes de 4 entiers. Cette taille correspond à la valeur la plus à droite dans la dimension (2,3,4). Plus on imbrique de niveaux, plus on obtient de dimensions : * 1 liste de 4 entiers est associée à un tableau de taille (4) * 3 listes de listes de 4 entiers sont associées à un tableau de taille (3,4). * 2 listes de 3 listes de listes de 4 entiers sont associées à un tableau de taille (2,3,4). A noter que la fonction *size* retourne le nombre d'éléments présents dans le tableau. Type de données --------------- Pour connaître le type utilisé pour stocker les valeurs, il est possible d'utiliser la propriété **dtype** : .. code-block:: python import numpy as np A = np.array( [0,1,2,3] ) print(A.dtype) ==> int32 .. quiz:: aaazzerzereaa :title: Vue et taille :quiz:`{"type":"TF","answer":"F"}` La fonction *reshape* crée toujours une vue. :quiz:`{"type":"TF","answer":"F"}` La fonction *reshape* crée toujours une copie. :quiz:`{"type":"TF","answer":"T"}` Les vues permettent d'optimiser l'utilisation de la mémoire. :quiz:`{"type":"TF","answer":"F"}` Les vues sont implémentées dans les tableaux NumPy mais pas pour les tenseurs de PyTorch ou de Tensorflow. :quiz:`{"type":"TF","answer":"F"}` Il n'existe aucun moyen de savoir si un tableau NumPy est associé à une vue. :quiz:`{"type":"FB","answer":"dim"}` Quelle fonction/propriété n'appartient pas à un tableau Numpy : dim, size ou shape ? :quiz:`{"type":"FB","answer":"dtype"}` Quelle propriété retourne le type des données stockées dans un tableau : type, dtype, ntype, xtype ? La documentation officielle NumPy ================================= On peut trouver dans la documentation officielle les très nombreuses fonctions fournies par la librairie NumPy classées par catégorie : * `Algèbre linéaire `_ * `Les fonctions mathématiques `_ * `Les traitements aléatoires `_ * `Statistiques `_ * ... Le paramètre axis ================= Les tableaux NumPy fournissent de nombreuses fonctions de calcul sur leurs valeurs internes. Par exemple, la fonction membre *mean()* calcule la moyenne des valeurs d'un tableau : .. code-block:: python import numpy as np A = np.array([[1,2,3,4],[6,7,8,9]]) A.mean() >> 5.0 Jusque là rien d'impressionnant, cependant, la fonction *mean()* grâce à l'argument *axis* permet d'effectuer le calcul dans une direction donnée, ce qui est très intéressant : .. code-block:: python import numpy as np A = np.array([ [[0,0,0],[2,2,2],[4,4,4],[6,6,6]], [[1,1,1],[3,3,3],[5,5,5],[7,7,7]] ]) A.shape >> (2, 4, 3) B = A.mean(axis=2) >> array([[0., 2., 4., 6.], [1., 3., 5., 7.]]) B.shape >> (2,4) La taille du tableau étant *(2,4,3)*, l'axe 0 correspond au 2, l'axe 1 au 4 et l'axe 2 au 3. Ainsi l'argument *axis=2* calcule la moyenne des éléments présents sur l'axe 2 ie la dernière dimension : .. math:: axis=2 \rightarrow B[x_0,x_1] = \underset{x_2}{\mathrm{mean}} (A[x_0,x_1,x_2]) Voici d'autres exemples pour :math:`axis = 0` et :math:`axis = 1` : .. code-block:: python B = A.mean(axis=0) >> array([[0.5, 0.5, 0.5], # B[0,0] = (A[0,0,0] + A[1,0,0]) / 2 [2.5, 2.5, 2.5], [4.5, 4.5, 4.5], [6.5, 6.5, 6.5]]) B.shape >> (4, 3) B = A.mean(axis=1) >> array([[3., 3., 3.], # B[0,0] = (A[0,0,0] + A[0,1,0] + A[0,2,0] + A[0,3,0]) / 4 [4., 4., 4.]]) B.shape >> (2,3) Cette logique fonctionne aussi pour plusieurs dimensions, ainsi pour :math:`axis = (1,2)`, nous avons : .. math:: axis=(1,2) \rightarrow B[x_0] = \underset{x_1,x_2}{\mathrm{mean}} (A[x_0,x_1,x_2]) .. code-block:: python B = A.mean(axis=(1,2)) >> array([3., 4.]) # taille (2,4,3) = > (2) B.shape >> (2) .. note:: De nombreuses fonctions proposent un paramètre *axis*, on peut citer déjà toutes celles similaires à *mean* : *max*, *min*, *sum* ou *std* (écart type). .. quiz:: zerfsdvfc :title: Axis Pour un tableau T = np.array([[1,2,3,4],[5,6,7,8]]), que retourne : T.max(axis=1) ? :quiz:`{"type":"FB","answer":"B"}` réponse A: [5 6 7 8] ou réponse B: [4 8] Pour un tableau T = np.array([[0,1,2],[3,4,5],[6,7,8]]), que faut-il écrire pour obtenir : [2 5 8] ? :quiz:`{"type":"FB","answer":"B"}` réponse A: T.max(axis=0) ou réponse B: T.max(axis=1) Pour T = np.array([ [[11,12],[13,14]], [[21,22],[23,24]], [[31,32],[33,34]] ]), que faut-il écrire pour obtenir [ [13,14], [23,24], [33,34] ] comme réponse ? :quiz:`{"type":"FB","answer":"B"}` réponse A: T.max(axis=0) ou réponse B: T.max(axis=1) ou réponse C: T.max(axis=2) Que faut-il écrire pour obtenir [33, 34] comme réponse avec le même tableau T ? :quiz:`{"type":"FB","answer":"A"}` réponse A: T.max(axis=(0,1)) ou réponse B: T.max(axis=(1,2)) ou réponse C: T.max(axis=(0,2)) Indexation ========== Vous pouvez retrouver ce chapitre dans `dans la documentation officielle `_ Indexation simple ----------------- L'indexation sur un tableau se fait en utilisant un tuple ou l'écriture compacte d'un tuple (sans les parenthèses) : .. code-block:: python import numpy as np A = np.array([ [[0,1,2],[3,4,5],[6,7,8]], [[10,11,12],[13,14,15],[16,17,18]] ]) print(A[(0,1,2)]) # avec un tuple => 5 print(A[0,1,2]) # forme allégée sans les ( ) => 5 .. warning:: Faîtes attention, car une autre syntaxe sans virgule existe : A[0][1][2]. Elle fournit le même résultat, mais elle a un rôle différent que nous vous présenterons juste après. .. note:: L'index le plus à droite correspond au plus bas niveau dans la liste. Il est possible d'utiliser des valeurs négatives, dans ce cas, la valeur -1 correspond au dernier élément, -2 à l'avant dernier et ainsi de suite : .. code-block:: python import numpy as np A = np.array([[0,1,2],[3,4,5],[6,7,8]]) A[1,-1] >> 5 A[-1,1] >> 7 A[-1,-1] >> 8 Sous-tableau ------------ Si la dimension du tuple est inférieure à la dimension du tableau, alors l'indexation permet d'extraire un sous-tableau correspondant à une vue : .. code-block:: python import numpy as np A = np.array([[[1,2],[3,4]],[[6,7],[8,9]]]) B = A[0] >> array([[1,2],[3,4]]) B.base is not None >> True # B est une vue C = A[0,1] >> array([3, 4]) C.base is not None >> True # C est une vue Plage d’indices - slicing ------------------------- Il est possible d'utiliser la syntaxe *start:stop* ou *start:stop:step* pour représenter une plage d’indices. Ce mécanisme, appelé **slicing**, permet d'éviter l'utilisation de boucles et facilite la lecture du code. Le tableau retourné correspond toujours à une vue. .. code-block:: python A = np.arange(10) >> array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) A[0:5] >> array([0, 1, 2, 3, 4]) A[0:8:2] >> array([0, 2, 4, 6]) A[:5] # tous les éléments jusqu'à l'indice 5 compris >> array([0, 1, 2, 3, 4]) A[5:] # tous les éléments après l'indice 5 >> array([5, 6, 7, 8, 9]) A[:] # Le symbole : représente tous les indices possibles >> array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Avec des tableaux 2 dimensions : .. code-block:: python A = np.array( [ [10,11,12,13], [20,21,22,23], [30,31,32,33], [40,41,42,43] ] ) A[1:3,1:3] # sélection des A[i,j] avec 1≤i<3 et 1≤j<3 >> array([[21, 22], [31, 32]]) A[1:3,1:3] = 99 # affectation des indices sélectionnées Ou encore : .. code-block:: python >> array([ [10, 11, 12, 13], [20, 99, 99, 23], [30, 99, 99, 33], [40, 41, 42, 43]]) A = np.array([ [1,2,3,4], [6,7,8,9] ]) A[:,:2] # toutes les lignes, tous les indices < 2 >> array([[1, 2], [6, 7]]) Ou encore : .. code-block:: python A = np.array( [ [10,11,12,13,14,15], [20,21,22,23,24,25], [30,31,32,33,34,35], [40,41,42,43,44,45] ] ) A[:, 0:6:2 ] = 99 # affection des colonnes 0 2 4 ==> array ([ [99, 11, 99, 13, 99, 15], [99, 21, 99, 23, 99, 25], [99, 31, 99, 33, 99, 35], [99, 41, 99, 43, 99, 45]]) on peut utiliser à la fois la technique des sous-tableaux avec le slicing : .. code-block:: python A = np.array([ [1,2,3,4], [6,7,8,9] ]) A[1] # 2eme ligne A[1,:] # idem >> array([6, 7, 8, 9]) A[0,2:] # 1ere ligne, tous les indices >=2 >> array([3, 4]) A[1,:2] # 2ème ligne, tous les indices < 2 >> array([6, 7]) Indexage avancé --------------- Cette technique se déclenche lorsque l'indexation se fait à partir de listes. Elle permet de sélectionner certains éléments :math:`(x_i,y_i)` du tableau en transmettant un liste de coordonnées :math:`(x_i)` et un liste de coordonnées :math:`(y_i)` pour désigner tous les index :math:`(x_i,y_i)` traités par l'opération : .. code-block:: python A = np.array( [ [10,11,12,13,14,15], [20,21,22,23,24,25], [30,31,32,33,34,35], [40,41,42,43,44,45] ] ) A[[0,0,3,3],[0,5,0,5]] # extrait les valeurs aux positions [0,0], [0,5], [3,0] et [3,5] >> array([10, 15, 40, 45]) A[[0,0,3,3],[0,5,0,5]] = 99 # affecte les valeurs aux positions [0,0], [0,5], [3,0] et [3,5] >> [[99 11 12 13 14 99] [20 21 22 23 24 25] [30 31 32 33 34 35] [99 41 42 43 44 99]] .. note:: Cette technique retourne toujours une copie contrairement à la syntaxe des sous-tableaux qui retourne toujours une vue. On peut cumuler la technique d'indexage avancé avec d'autres : .. code-block:: python A = np.array( [ [10,11,12,13,14,15], [20,21,22,23,24,25], [30,31,32,33,34,35], [40,41,42,43,44,45] ] ) A[[1,2],:] # extrait la 2ème et la 3ème ligne du tableau >>array([[20, 21, 22, 23, 24, 25], [30, 31, 32, 33, 34, 35]]) A[:,[1,2]] # extrait la 2ème et la 3ème colonne du tableau array([[11, 12], [21, 22], [31, 32], [41, 42]]) .. warning:: Comment savoir si la librairie NumPy a implémenté la syntaxe à laquelle vous pensez ? Réponse difficile, car c'est du cas par cas, mieux vaut tester pour être sûr ! .. quiz:: azeraazeaa :title: Utilisation des tableaux NumPy :quiz:`{"type":"FB","answer":"1,0"}` Pour A = np.array([ [0,1],[2,3],[4,5]]), donnez l'indexation de A retournant la valeur 2 sous la forme : x,y. :quiz:`{"type":"FB","answer":"0,2,1"}` Pour A = np.array([[[0,1],[2,3],[4,5]]]), donnez l'indexation de A retournant la valeur 5 (indices positifs). :quiz:`{"type":"FB","answer":"6"}` Pour un tableau 1D, si A[-3] désigne la même cellule que A[3], quelle est la taille de ce tableau ? :quiz:`{"type":"FB","answer":"0 7 2 9"}` Pour le tableau A : [[0,1,2],[4,5,6],[7,8,9]], quels sont les chiffres extraits par : A[[0,2,0,2],[0,0,2,2]]. Donnez les chiffres séparés par des espaces. TD1 Numpy Array =============== `Le source du Notebook `_ Vous devez effectuer ce TD et le faire valider à votre responsable de salle.